一、MotionEvent
MotionEvent 是 Android 系统中报告动作输入(如触摸屏幕,滚动球,鼠标)等事件的类。其中有整形数的 Action 和描述事件的各种浮点型坐标。MotionEvent 的 action 的最低一个字节,其取值我们常见的 ACTION_DOWN, MOVE, POINTER_DOWN, POINTER_UP, UP, CANCEL (后面可能简称为 DOWN 等等) 这些了。虽然我们可能会粗暴的使用下面这样的方式:
1 2 3 4 5 6 switch (event.getAction()) { case MotionEvent.ActionDown: ... break ; ... }
但是其实上面这样仅适用于单点触控场景,原因是 getAction() 方法返回的结果中,最低的1个字节才对应上面说的 ACTION_DOWN,而次最低一个字节则表示 PointerIndex (这个概念后面会提到),对于单点触控场景,PointerIndex 始终为0,所以上面的代码才能跑通。更好的方式,其实是使用 getActionMasked() 方法,此处查看源码即可非常清晰。
对于一串触摸事件(指手接触到屏幕到手完全离开屏幕),用户第一个手指接触到屏幕时,触发 DOWN, 手指在屏幕上移动时,持续触发出很多 MOVE,第二个以上的手指触摸到屏幕是,触发 POINTER_DOWN,非最后一个离开屏幕的手指离开屏幕时触发 POINTER_UP, 最后一个手指离开屏幕时,触发 UP。
在一串触摸事件过程中,视图从 window detach 的话,或者 View 的 PFLAG_CANCEL_NEXT_UP_EVENT 标记位设置了时,或者 View 本来处理了一个 Down 事件但是接下来的 MOVE 等事件被 ViewGroup 拦截了,将会触发 CANCEL。当然,应该还有别的触发 CANCEL 事件的场景。下图是一串 Touch 事件的例子:
Down → Move →...→ Move → PointerDown → Move →...→ PointerUp → Move →...→ Move → Up
MotionEvent 中有个 Pointer 的概念,Pointer 大致表示触摸到屏幕的各个手指。getPointerCount() 即为多点触控的点数。每个 pointer 都有一个 index (取值是0到getPointerCount()-1)和 id(最大是31,意味着一串触摸事件中,最多允许手指接触屏幕32次),前者在一串 Touch 事件中可能会变化,而后者保持不变,例如多点触控时,大拇指对应的 index 本来是0,后面可能变成了1了. getPointerId(int pointerIndex)和findPointerIndex(int pointerId)这两个方法是 index 和 id 间的一一映射。 那为什么要有 index 和 id 两个东西呢?
index 和 id 的区别。举个例子就清楚了。假设起始时,右手的食指,中指,无名指依次先后落在屏幕上,那么三个指头的 index 和 id 分别都是 0, 1, 2。此时抬起中指,那么食指的 index 和 id 不变,而无名指的 index 变成1,id 依然是2。即 index 总是 0, 1, 2,…, getPointerCount() - 1. 而 id 则可能大于 getPointerCount(), 另一面则是,pointer 的 id 在整个一串触摸事件中保持不变。
MotionEvent 的 getX() 与 getX(int pointerIndex), 前者等价于后者的 getX(0), getY 类似. getPointerIdBits() 返回一个类似于标志数的东西,其二进制从右向左数,第 i 位为1表示存在 id 是 i 的 pointer,此处应看源码。可以想见此结果的二进制形式中1的个数即为多点触控的点数。
二、Touch 事件的生成
Touch 事件的根源是从硬件而来,而 ViewRootImpl$ViewPostImeInputStage#processPointerEvent 方法是一个比较合适的开始追踪 Touch 事件的起点,此处应看源码。事件如果没有被消费,最终可能将在 ViewRootImpl #finishInputEvent 中回收掉(这不是本文的重点)。Touch 事件的传递堆栈 (按调用的顺序排列):
1 2 3 4 5 6 7 ViewRootImpl$ ViewPostImeInputStage View# dispatchPointerEvent DecorView# dispatchTouchEvent Activity# dispatchTouchEvent PhoneWindow# superDispatchTouchEvent DecorView# superDispatchTouchEvent ViewGroup# dispatchTouchEvent
Touch 事件传递到了 ViewGroup 的 dispatchTouchEvent 方法后,就开始是本文要关注的焦点了,即一串 Touch 事件在 View 树上是如何传递和消费的。
三、传递和处理 Touch 事件
下面是事件传递和处理中最重要的 4 个方法:
1 2 3 4 5 6 7 View# dispatchTouchEvent,非 ViewGroup 的 View 接受 Touch 事件的方法,我们一般不重载它。 View# onTouchEvent,View(含 ViewGroup)自己尝试去消费 Touch 事件,经常会被重载。 ViewGroup# dispatchTouchEvent,重载了 View 的同名方法,作为 ViewGroup 接受 Touch 事件,此方法中包含「传递 Touch 事件给 child」和「尝试自己消费 Touch 事件」这样两个分支逻辑,一般不重载它。 ViewGroup# onInterceptTouchEvent,ViewGroup 判断自己要不要拦截 Touch 事件的方法,拦截意味着自己尝试消费此事件,有时候被重载。
传递:
在 View 树中,Touch 事件是由 parent view 传递给 child View (child view 也可能是 ViewGroup)的。在一串 touch 事件中打头的总是 DOWN 事件,parent view 通过调用 child view 的 dispatchTouchEvent方法把 DOWN 事件传递给 child view,这就给了 child 一个消费此事件的机会。child 的 dispatchTouchEvent 方法返回 true 的话,就表示 child 消费了 DOWN 事件,返回 false 就表示不消费。如果 child 消费了 Down 事件,那么最简单的情况就是后续的 move 等事件都会通过调用 child 的 dispatchTouchEvent 方法直接交给此 child,不管它的 dispatchTouchEvent 方法返回什么;如果 child 没有消费 DOWN 事件,那么单点触控情形下,后续的 move 等事件与此 child 再也无缘了。
消费:
View(包括 ViewGroup) 消费 Touch 事件,在代码上等价于调用当前类的 onTouchEvent 方法。然后 ouTouchEvent 的返回结果作为是否消费了此事件的依据。
拦截:
拦截是指 ViewGroup 有机会在把 touch 事件 dispatch 给 child 前,通过调用自己的 onInterceptTouchEvent 来判断要不要把 touch 事件截下来给自己尝试消费。对于 DOWN 事件,onInterceptTouchEvent 总是会被调用;对于其他事件,child 有机会在 parent 的onInterceptTouchEvent 被调用之前,请求 parent 不要拦截,这个请求就是通过 child 调用 parent 的 requestDisallowInterceptTouchEvent 方法来实现的。
DOWN 事件:
DOWN 事件是一串 touch 事件的第一个事件,在 ViewGroup 的 dispatchTouchEvent 方法中,接到此事件时,首先会做一些清除工作,然后检查自己要不要拦截此事件和是不是要转为 CANCEL 事件。如果不拦截且不转为 CANCEL,那么挨个检查 child,在恰当的时候调用 child 的 dispatchTouchEvent 来看有没有 child 会消费此事件。
上面 4 段话,用伪代码来表示,大概就是下面这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class ViewGroup extends View { public boolean dispatchTouchEvent (MotionEvent event) { final boolean intercepted; boolean handled = false ; if (is DOWN || child ever comsumed previous event) { if (! disallowIntercept) { intercepted = onInterceptTouchEvent(event); } else { intercepted = false ; } } else { intercepted = true ; } if (! canceled && !intercepted) { for (View child : all children) { if (! child able to consume event) { continue ; } if (dispatchTransformedTouchEvent(event, child)) { break ; } } } if (no child ever consumed previous event) { handled = super .dispatchTransformedTouchEvent(event); } else { for (child : children who has consumed previous event) { if (child.dispatchTouchEvent(event) handled = true ; } } return handled; } } class View { public boolean dispatchTouchEvent (MotionEvent event) { boolean result = false ; if (mOnTouchListener != null && isEnabled && mOnTouchListener.onTouch(this , event)) { result = true ; } if (! result && onTouchEvent(event)) { result = true ; } return result; } public boolean onTouchEvent (MotionEvent event) { if (!isEnabled()) { return isClickable() || isLongClickable(); } if (isClickable || isLongClickable) { ... retrun true ; } retrun false ; } }
各个类和方法的角色描述如下:
ViewGroup#dispatchTouchEvent,代表 ViewGroup 接受事件,处理拦截逻辑,尝试分发事件给 child,调用 View#dispatchTouchEvent来尝试自行消费 Touch 事件。返回值表示自己这颗子树有没有消费事件。
View#dispatchTouchEvent 代表非 ViewGroup 的 View 接受事件,调用真正尝试消费事件的代码(onTouchEvent 或者 onTouch 之类的方法)。返回值表示自己有没有消费事件。
ViewGroup#onInterceptTouchEvent,返回值表示 ViewGroup 要不要拦截事件。
ViewGroup#requestDisallowIntercept,被 child 来调用,表示请求 parent 不要拦截,这个请求仅在非 DOWN 事件有效,且会递归向上调用所有 parent 的同名方法。
TouchTarget 类,此类包含消费过 Touch 事件的 child 和它消费过哪些 pointer 的事件这些信息,TouchTarget 如下图所示。在 ViewGroup 中,存有一个 TouchTarget 链表,遍历此链表,即可知道一个 pointer 是否已有事件被某个 child 处理过。
以上的介绍,其实是省略了不少信息,真正的事件分发过程,要更复杂不少,下面是我尝试画的一张流程图 (把图和源码对照着理解可能会有些帮助):
对于上图,有一些需要解释的地方:
TouchTarget 持有一个 child View,和此 child View 曾消费过 Down 或者 pointerDown 事件的那些 Pointer 的 id,这些 pointer id 是以 id bits 的形式存储为一个整数的。
TouchTarget 链表的头结点是由 mFirstTouchTarget 引用的。在一串事件结束(处理完 UP)后,正常情况链表应该清空,在一串 Touch 事件到来前(处理 Down 前)也会清空,算是补刀。
链表的意义是,存储 child 曾经消费过某些 pointer 的 Down 或者 PointerDown 事件这种信息。当一个 Pointer 结束了(手指头离开屏幕),那么所有消费过它的 Down 或者 PointerDown 事件的 TouchTarget 都需要移除掉它的 id,事实上,这一串 touch 事件中再也不会有这个 id 了;如果一个 TouchTarget 的 pointer id移除光了,那么意味着此 TouchTarget 持有的 child 没有消费过任意(现存)的 pointer 的 Down 或者 PointerDown,于是可以把此 TouchTarget 从链表移除了。
每次一个 Down 或者 PointerDown 事件 ev 到来时,对于 ViewGroup 的每个 child x,若 ev 的坐标落在 x 的范围内(否则就 continue,考虑下一个 child),进一步「如果 x 在链表中(说明 x 消费过 Down 或者 pointerDown 事件),那么 x 就是要被分发的 child;否则如果 x.dispatchTouchEvent(ev) 返回 true 了,那么 x 同样是要被分发的 child,虽然此时分发已经结束了」。
每个 move 事件 ev 到来时,链表为空的话,显然没有 child 消费过 down 或者 pointerDown,那么直接让 viewGroup 处理 ev 就好了。链表不为空的话,对于链表的每个 TouchTarget t 持有的 child x:如果转成了 cancel 事件,那么向 x 分发一个 cancel 事件,另外把 t 从链表移除(移除的原因是,例如 x 正在 detaching,所以才引发 cancel,那么当然需要把持有 x 的 t 移除),然后 over,即 move 事件丢失了;没转成 cancel,那就检查 t 的 idBits 中有没有 ev 的任意一个 pointer 的 id,有则把 ev 交给 x,没有则 continue。
四、源码分析
下面粘贴一大段加上了我的理解作为注释的源码,代码真的很长很长,这还仅仅是 ViewGroup#dispatchTouchEvent 这一个方法的代码。关于多点触控的处理逻辑,我也没有彻底明白,sigh。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 @Override public boolean dispatchTouchEvent (MotionEvent ev) { boolean handled = false ; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null ) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 ; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false ; } } else { intercepted = true ; } final boolean canceled = resetCancelNextUpFlag(this ) || actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 ; TouchTarget newTouchTarget = null ; boolean alreadyDispatchedToNewTouchTarget = false ; if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0 ) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1 ; i >= 0 ; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null )) { ev.setTargetAccessibilityFocus(false ); continue ; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null ) { newTouchTarget.pointerIdBits |= idBitsToAssign; break ; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false , child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null ) { for (int j = 0 ; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break ; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true ; break ; } ev.setTargetAccessibilityFocus(false ); } if (preorderedList != null ) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null ) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null ) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } if (mFirstTouchTarget == null ) { handled = dispatchTransformedTouchEvent(ev, canceled, null , TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null ; TouchTarget target = mFirstTouchTarget; while (target != null ) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true ; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true ; } if (cancelChild) { if (predecessor == null ) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue ; } } predecessor = target; target = next; } } if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null ) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1 ); } return handled; }
五、滑动冲突分析示例
在《安卓开发艺术探索》一书中讲 touch 事件的最后,讲了滑动冲突的解决方法。所谓滑动冲突最简单的情形就是一个水平滑动的 ScrollView 里面放一个竖直滑动的 listView,两者滑动方向不同,即为滑动冲突。对于这个情形,解决滑动冲突,其实就是在手指上下滑时吧 move 事件给 child(listView)处理,而手指左右滑动时给 parent 处理(ScrollView)即可。下面是书中的一种解决方式,使用上面讲的知识,我们可以透彻的分析这种解决方式。
下面是《安卓开发艺术探索》中提供的其中一种解决方法,我加入了比较详细的注释作为解释,在前面的基础上,这种解决方法的逻辑就很明确了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class ChildView extends android .view .View { @Override public boolean dispatchTouchEvent (MotionEvent event) { ViewParent parent = getParent(); if (parent != null ) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true ); break ; case MotionEvent.ACTION_MOVE: if (parent should handle event){ parent.requestDisallowInterceptTouchEvent(false ); } break ; default : break ; } } return super .dispatchTouchEvent(event); } } public class ParentView extends ViewGroup { @Override public boolean onInterceptTouchEvent (MotionEvent ev) { return ev.getActionMasked() != MotionEvent.ACTION_DOWN; } }
到此,这篇长长的、并不完美的分析也就结束了。